# frozen_string_literal: true

require('fiber')
require('./lib')

class Solver
  include Singleton

  def solving?
    return @fiber&.alive?
  end

  def resume
    return @fiber.resume if solving?

    puts("warning: no-op'ing due to dead fibre")
  end

  def begin_solving(b)
    @fiber = Fiber.new do
      shortest_solutions(b)
    end
  end

  def shortest_solutions(b)
    BoardHistory.instance.clear
    return shortest_solutions_bfs(b).tap do |solutions|
      dispatch_event(:solutions_found, solutions)
      Fiber.yield
    end
  end

  def shortest_solutions_bfs(b)
    return shortest_solutions_bfs_iter([[Move.new(b, nil, nil)]], 0)
  end

  def shortest_solutions_bfs_iter(paths, depth)
    paths.flat_map do |path|
      path.last.board.legal_moves.reject do |m|
        BoardHistory.instance.seen_equiv_at_or_deeper?(m.board, depth)
      end.collect do |m|
        BoardHistory.instance.saw_at_depth(m.board, depth)
        path + [m]
      end
    end.tap do |candidate_paths|
      dispatch_event(:trying_candidates, candidate_paths, depth)
      Fiber.yield
    end.partition do |candidate_path|
      Game.instance.winning?(candidate_path.last.board)
    end.tap do |winning_paths, remaining_paths|
      return shortest_solutions_bfs_iter(remaining_paths, depth + 1) if winning_paths.empty?

      dispatch_event(:wins_found, winning_paths, depth)
      Fiber.yield
      return winning_paths
    end
  end

  def dispatch_event(method, *args)
    @observable&.dispatch_event(method, *args)
  end

  def observable(&block)
    block&.call(@observable ||= Observable.new)
    return @observable
  end
end
